package cn.gs.global.server.account;

// @author gs
import cn.gs.algorithm.NumberSplitUtil;
import cn.gs.algorithm.graph.WeightedGraph;
import cn.gs.db.HibernateUtil;
import cn.gs.exchanger.ExchangerException;
import cn.gs.global.common.meta.IObjServer;
import cn.gs.global.common.meta.IOgnServer;
import cn.gs.global.common.meta.ObjInfo;
import cn.gs.global.common.meta.OgnSnapshot;
import cn.gs.global.table.account.SimKeep;
import cn.gs.global.table.account.SimOgn;
import cn.gs.global.table.exchg.ObjExchg;
import cn.gs.util.set.ArrayUtil;
import cn.gs.util.set.Cache;
import java.util.Map.Entry;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.hibernate.Query;

public class MonthAccount implements Runnable {

    public enum Status {

        READY, RUNNING, FINISH, FAIL;
    }
    //Outside
    Cache<Integer, ObjInfoSplit> caches;
    IOgnServer ognsvr;
    IObjServer objsvr;
    HibernateUtil util;
    SimOgn ogn;
    int checkmonth;
    //inside
    public Status status = Status.READY;
    Map<Integer, ObjItem> items;
    //ObjID(pos is ManageCost, neg is ProdCost)  
    WeightedGraph<Integer, Double, Double> g;
    OgnSnapshot sns;
    //long[cost][prod]
    long[][] assign;
    int[] prod, cost;
    int month;

    public MonthAccount(Cache<Integer, ObjInfoSplit> caches, IOgnServer ognsvr, IObjServer objsvr, HibernateUtil util, SimOgn ogn) {
        this.caches = caches;
        this.ognsvr = ognsvr;
        this.objsvr = objsvr;
        this.util = util;
        this.ogn = ogn;
    }

    public void run() {
        Thread.currentThread().setName("MonthAccount:" + ogn.getId());
        status = Status.RUNNING;
        //util.getSession().refresh(ogn);
        month = ogn.getLastcheck() + 1;
        OgnSnapshot[] snss;
        try {
            snss = ognsvr.snapshot(month, ogn.getId());
        } catch (ExchangerException ex) {
            Logger.getLogger(MonthAccount.class.getName()).log(Level.SEVERE, null, ex);
            return;
        }
        if (null == snss || null == snss[0]) {
            Logger.getLogger(MonthAccount.class.getName()).log(Level.SEVERE, "Can not get Ogn Snapshot from Ogn:{0} of month{1}", new Object[]{ogn.getId(), month});
            status = Status.FAIL;
            return;
        }
        items = new HashMap<>();
        sns = snss[0];
        fillBeginOgnInfo();
        fillEndOgnInfo();
        addBought();
        addSold();
        fillManageCost();
        fillProdCost();
        cost = checkCost();
        if (cost.length > 0) {
            loadInfos(items.keySet());
            prod = checkProd();
            buildGraph();
            buildProdGraph();
            totalConsumeUsed();
            calcMatrix();
            totalCost();
            updateOgnKeep();
        }
        ogn.setLastcheck(month);
        util.transactionSaveOrUpdate(ogn);
        status = Status.FINISH;
    }

    ObjItem getItem(int objId) {
        ObjItem it = items.get(objId);
        if (null == it) {
            it = new ObjItem();
            items.put(objId, it);
        }
        return it;
    }

    void totalCost() {
        for (int i = 0; i < prod.length; ++i) {
            ObjItem it = getItem(Math.abs(prod[i]));
            for (int j = 0; j < cost.length; ++j) {
                if (assign[j] != null) {
                    it.newCost += assign[j][i];
                }
            }
        }
    }

    void updateOgnKeep() {
        long unassign = 0;
        for (SimKeep k : ogn.getKeeps()) {
            ObjItem it = getItem(k.getObj());
            k.setProduce(k.getProduce() + it.newCost);
            k.setCnt(it.endCnt);
            k.setExi(it.exi);
            unassign += it.manageCost + it.prodCost;
            items.remove(k.getObj());
        }
        for (Entry<Integer, ObjItem> e : items.entrySet()) {
            unassign += e.getValue().manageCost + e.getValue().prodCost;
            SimKeep k = new SimKeep();
            k.setObj(e.getKey());
            k.setOgn(ogn.getId());
            k.setProduce(e.getValue().newCost);
            k.setCnt(e.getValue().endCnt);
            k.setExi(e.getValue().exi);
            ogn.getKeeps().add(k);
        }
        ogn.setUnassigned(ogn.getUnassigned() + unassign);
    }

    public void printAssign() {
        if (null == cost || null == prod) {
            System.out.println("Nothing.");
            return;
        }
        for (int i = 0; i < prod.length; ++i) {
            System.out.print(prod[i]);
            System.out.print(" ");
        }
        System.out.println();
        for (int j = 0; j < cost.length; ++j) {
            System.out.print(cost[j]);
            System.out.print(":");
            if (assign[j] != null) {
                for (int i = 0; i < prod.length; ++i) {
                    System.out.print(" ");
                    System.out.print(assign[j][i]);
                }
                System.out.println();
            } else {
                System.out.println("Unassigned");
            }
        }
    }

    void addBought() {
        Query q = util.query("FROM ObjExchg WHERE frm=:frm AND extime BETWEEN :begin AND :end");
        q.setParameter("frm", ogn);
        q.setParameter("begin", month * 30 - 30);
        q.setParameter("end", month * 30 - 1);
        List<ObjExchg> list = q.list();
        for (ObjExchg e : list) {
            ObjItem it = getItem(e.getObj());
            it.bought += e.getCnt() * 100;
        }
    }

    void addSold() {
        Query q = util.query("FROM ObjExchg WHERE too=:too AND extime BETWEEN :begin AND :end");
        q.setParameter("too", ogn);
        q.setParameter("begin", month * 30 - 30);
        q.setParameter("end", month * 30 - 1);
        List<ObjExchg> list = q.list();
        for (ObjExchg e : list) {
            ObjItem it = getItem(e.getObj());
            it.sold += e.getCnt();
        }
    }

    void recursiveCalMatrix(double[][] m, Map<Integer, Integer> costindex, Set<Integer> visited, int node, double rate, int prod) {
        if (visited.add(node)) {
            Integer index = costindex.get(node);
            if (index != null) {
                m[index][prod] += rate;
            }
            for (Entry<Integer, Double> e : g.weightedInners(node).entrySet()) {
                double total = g.nodeValue(e.getKey());
                recursiveCalMatrix(m, costindex, visited, e.getKey(), rate * e.getValue() / total, prod);
            }
            visited.remove(node);
        }
    }

    void calcMatrix() {
        double[][] m = new double[cost.length][prod.length];
        //Index costobj
        Map<Integer, Integer> costindex = new HashMap<>();
        for (int i = 0; i < cost.length; ++i) {
            costindex.put(cost[i], i);
        }
        //Calc assign rate
        for (int i = 0; i < prod.length; ++i) {
            double cc = g.edgeWeight(prod[i], 0);
            double total = g.nodeValue(prod[i]);
            recursiveCalMatrix(m, costindex, new HashSet<Integer>(), prod[i], cc / total, i);
        }
        //assign
        assign = new long[cost.length][prod.length];
        for (int i = 0; i < cost.length; ++i) {
            if (cost[i] > 0) {
                ObjItem it = getItem(cost[i]);
                assign[i] = NumberSplitUtil.split(true, it.manageCost, m[i]);
                if (assign[i] != null) {
                    it.manageCost = 0;
                }
            } else {
                ObjItem it = getItem(-cost[i]);
                assign[i] = NumberSplitUtil.split(true, it.prodCost, m[i]);
                if (assign[i] != null) {
                    it.prodCost = 0;
                }
            }
        }
    }

    void totalConsumeUsed() {
        for (Integer obj : g.nodes()) {
            double sum = 0;
            for (double e : g.weightedOuters(obj).values()) {
                sum += e;
            }
            g.setNodeValue(obj, sum);
        }
    }

    void outLink(long exi, int obj, Map<Integer, Double> add, boolean mg) {
        int sign = (mg ? 1 : -1);
        double[] assign = new double[add.size()];
        int i = 0;
        for (Entry<Integer, Double> e : add.entrySet()) {
            ObjItem it = getItem(e.getKey());
            assign[i++] = e.getValue() * it.price;
        }
        assign = NumberSplitUtil.one(assign);
        i = 0;
        for (Entry<Integer, Double> e : add.entrySet()) {
            double part = assign[i++];
            g.addOutLink(obj, e.getKey() * sign, exi * part);
        }
    }

    void inLink(long exi, int obj, Map<Integer, Double> its, boolean mg) {
        //All cost into Manage Cost
        int sign = (mg ? 1 : -1);
        for (Entry<Integer, Double> e : its.entrySet()) {
            g.addOutLink(e.getKey() * sign, obj, -exi * e.getValue());
        }
    }

    void link(long exi, Map<Integer, Double> add, Map<Integer, Double> sub, boolean addmg, boolean submg) {
        int adds = (addmg ? 1 : -1), subs = (submg ? 1 : -1);
        //calc cost assignment to each product
        double[] assign = new double[add.size()];
        int i = 0;
        for (Entry<Integer, Double> e : add.entrySet()) {
            ObjItem it = getItem(e.getKey());
            assign[i++] = e.getValue() * it.price;
        }
        assign = NumberSplitUtil.one(assign);
        //assign consume cost to each product
        i = 0;
        for (Entry<Integer, Double> e : add.entrySet()) {
            double part = assign[i++];
            for (Entry<Integer, Double> f : sub.entrySet()) {
                g.addOutLink(f.getKey() * subs, e.getKey() * adds, exi * -f.getValue() * part);
            }
        }
    }

    void buildGraph() {
        g = new WeightedGraph<>();
        for (Entry<Integer, ObjItem> e : items.entrySet()) {
            ObjInfoSplit f = caches.get(e.getKey());
            if (f.provide.size() > 0) {
                if (f.prod.size() > 0) {
                    link(e.getValue().exi, f.provide, f.used, true, true);
                    link(e.getValue().exi, f.prod, f.consume, false, false);
                    //Product Cost also include ManageCost of material
                    link(e.getValue().exi * 30, f.prod, f.consume, false, true);
                } else {
                    link(e.getValue().exi, f.provide, f.used, true, true);
                    link(e.getValue().exi, f.provide, f.consume, true, false);
                    //Manage Cost also include ManageCost of material
                    link(e.getValue().exi * 30, f.provide, f.consume, true, true);
                }
                outLink(e.getValue().exi, e.getKey(), f.provide, true);
            } else if (f.prod.size() > 0) {
                link(e.getValue().exi, f.prod, f.used, false, true);
                link(e.getValue().exi, f.prod, f.consume, false, false);
                link(e.getValue().exi * 30, f.prod, f.consume, false, true);
                outLink(e.getValue().exi, e.getKey(), f.prod, false);
            } else {
                //nothing can transfer these cost
                inLink(e.getValue().exi, e.getKey(), f.used, true);
                inLink(e.getValue().exi, e.getKey(), f.consume, false);
                inLink(e.getValue().exi * 30, e.getKey(), f.consume, true);
            }
        }
    }

    void buildProdGraph() {
        for (int i = 0; i < prod.length; ++i) {
            ObjItem it = getItem(Math.abs(prod[i]));
            int cnt = it.endCnt + it.sold - it.beginCnt - it.bought;
            if (cnt > 0) {
                g.addOutLink(prod[i], 0, 1.0 * cnt);
            }
        }
    }

    void loadInfos(Collection<Integer> ids) {
        if (caches.getMaxCount() < ids.size()) {
            caches.setMaxCount(ids.size());
        }
        Set<Integer> vs = new HashSet<>();
        for (Integer q : ids) {
            ObjInfoSplit ii = caches.get(q);
            if (null == ii) {
                vs.add(q);
            }
        }
        int[] id = ArrayUtil.collection2Array(vs);
        ObjInfo[] infos;
        try {
            infos = objsvr.info(id);
            for (ObjInfo info : infos) {
                caches.put(info.getObjId(), new ObjInfoSplit(info));
            }
        } catch (ExchangerException ex) {
            Logger.getLogger(MonthAccount.class.getName()).log(Level.WARNING, null, ex);
        }
    }

    int[] checkProd() {
        Set<Integer> ida = new HashSet<>();
        for (Entry<Integer, ObjItem> e : items.entrySet()) {
            ObjItem it = e.getValue();
            if (it.endCnt + it.sold > it.beginCnt + it.bought) {
                ida.add(e.getKey());
                ida.add(-e.getKey());
            }
        }
        Set<Integer> idb = new HashSet<>();
        for (Entry<Integer, ObjItem> e : items.entrySet()) {
            ObjInfoSplit ois = caches.get(e.getKey());
            for (Integer ii : ois.prod.keySet()) {
                idb.add(ii);
                idb.add(-ii);
            }
        }
        ida.retainAll(idb);
        return ArrayUtil.collection2Array(ida);
    }

    int[] checkCost() {
        List<Integer> ids = new LinkedList<>();
        for (Entry<Integer, ObjItem> e : items.entrySet()) {
            if (e.getValue().manageCost > 0) {
                ids.add(e.getKey());
            }
            if (e.getValue().prodCost > 0) {
                ids.add(-e.getKey());
            }
        }
        return ArrayUtil.collection2Array(ids);
    }

    void fillEndOgnInfo() {
        for (int i = 0, len = sns.getIds().length; i < len; ++i) {
            ObjItem it = getItem(sns.getIds()[i]);
            it.endCnt = sns.getCnts()[i];
            it.exi = sns.getExis()[i];
        }
    }

    void fillBeginOgnInfo() {
        for (SimKeep k : ogn.getKeeps()) {
            ObjItem it = getItem(k.getObj());
            it.beginCnt = k.getCnt();
            it.exi += -k.getExi();
            it.price = (0 != k.getPerPrice() ? k.getPerPrice() : k.getCnt() > 0 ? (k.getBuy() + k.getProduce()) / (double) k.getCnt() : 0);
        }
    }

    void fillManageCost() {
        for (SimKeep k : ogn.getKeeps()) {
            ObjItem it = getItem(k.getObj());
            //Manage Cost for bought
            long mgc = 0;
            if (k.getBuy() <= CAccountSetting.MIN_MANAGE_COST || k.getBuy() < CAccountSetting.BOUGHT_MANAGE_COST_PERCENT) {
                mgc = k.getBuy();
            } else {
                mgc += k.getBuy() / CAccountSetting.BOUGHT_MANAGE_COST_PERCENT;
            }
            it.manageCost += mgc;
            k.setBuy(k.getBuy() - mgc);
            mgc = 0;
            //Manage Cost for product
            if (k.getProduce() <= CAccountSetting.MIN_MANAGE_COST || k.getProduce() < CAccountSetting.PROD_MANAGE_COST_PERCENT) {
                mgc += k.getProduce();
            } else {
                mgc += k.getProduce() / CAccountSetting.PROD_MANAGE_COST_PERCENT;
            }
            it.prodCost += mgc;
            k.setProduce(k.getProduce() - mgc);
        }
    }

    void fillProdCost() {
        for (SimKeep k : ogn.getKeeps()) {
            ObjItem it = getItem(k.getObj());
            if (it.beginCnt + it.bought > it.endCnt + it.sold) {
                long[] cost;
                //Prod Cost for bought
                cost = NumberSplitUtil.split(k.getBuy(), it.endCnt + it.sold, it.beginCnt + it.bought - it.endCnt - it.sold);
                k.setBuy(cost[0]);
                it.prodCost += cost[1];
                //Prod Cost for product
                cost = NumberSplitUtil.split(k.getProduce(), it.endCnt + it.sold, it.beginCnt + it.bought - it.endCnt - it.sold);
                k.setProduce(cost[0]);
                it.prodCost += cost[1];
            }
        }
    }

    class ObjItem {

        int beginCnt, endCnt, sold, bought;
        double provide, used, produce, consume, price;
        long manageCost, prodCost, newCost, exi;
    }

    public static class ObjInfoSplit {

        Map<Integer, Double> provide = new HashMap<>(), used = new HashMap<>(), prod = new HashMap<>(), consume = new HashMap<>();

        public ObjInfoSplit(ObjInfo f) {
            for (int i = 0, len = f.getIds().length; i < len; ++i) {
                if (f.getFols()[i] > 0) {
                    provide.put(f.getIds()[i], f.getFols()[i]);
                } else if (f.getFols()[i] < 0) {
                    used.put(f.getIds()[i], f.getFols()[i]);
                }
                if (f.getChgs()[i] > 0) {
                    prod.put(f.getIds()[i], f.getChgs()[i]);
                } else if (f.getChgs()[i] < 0) {
                    consume.put(f.getIds()[i], f.getChgs()[i]);
                }
            }
        }
    }
}
